﻿//   VirtualMachine.cs
//
//   Copyright 2009 Eric Llewellyn
//
//   Licensed under the Apache License, Version 2.0 (the "License");
//   you may not use this file except in compliance with the License.
//   You may obtain a copy of the License at
//
//       http://www.apache.org/licenses/LICENSE-2.0
//
//   Unless required by applicable law or agreed to in writing, software
//   distributed under the License is distributed on an "AS IS" BASIS,
//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//   See the License for the specific language governing permissions and
//   limitations under the License.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace LibEELbeta
{
    public delegate void FunctionCallDelegate(VirtualMachine stackMac);
    public delegate void ReadDelegate(object FunctionOnRead);
    public delegate void WriteDelegate(string str);
    public delegate void RuntimeErrorDelegate(string message, int instructionNumber);

    public class VirtualMachine
    {
        private Stack<object> stack = new Stack<object>();
        private Dictionary<object, object> globals = new Dictionary<object, object>();
        private Dictionary<object, object> locals = new Dictionary<object, object>();
        private Stack<Dictionary<object, object>> varStack = new Stack<Dictionary<object, object>>();
        private int ip = 0;
        private Stack<int> returnStack = new Stack<int>();
        private List<Instruction> instructions = new List<Instruction>();
        private Dictionary<string, int> jmpAddresses = new Dictionary<string, int>();
        public ReadDelegate ReadFunction;
        public WriteDelegate WriteFunction;
        public event RuntimeErrorDelegate RuntimeError;

        public static string Version
        {
            get
            {
                return "LibEEL " +
                    System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString();
            }
        }

        public void LoadProgram(Instruction[] ins)
        {
            foreach (Instruction i in ins)
            {
                if (i.opCode == InstructionCodes.Label)
                    jmpAddresses[(string)i.data] = instructions.Count;
                else
                    instructions.Add(i);
            }
        }

        public void Reset()
        {
            globals = new Dictionary<object, object>();
            stack = new Stack<object>();
            returnStack = new Stack<int>();
            varStack = new Stack<Dictionary<object, object>>();
            ip = 0;
            instructions = new List<Instruction>();
        }

        public void Push(object value)
        {
            stack.Push(value);
        }

        public object Pop()
        {
            if (stack.Count < 1)
            {
                RuntimeError.Invoke("Attempting to pop from empty stack", ip);
                return null;
            }
            else
                return stack.Pop();
        }

        public void Load()
        {
            object key = Pop();
            if (locals.ContainsKey(key))
                Push(locals[key]);
            else
                Push(null);
        }

        public void Store()
        {
            object key = Pop();
            locals[key] = Pop();
        }

        public void GLoad()
        {
            object key = Pop();
            if (globals.ContainsKey(key))
                Push(globals[key]);
            else
                Push(null);
        }

        public void GStore()
        {
            object key = Pop();
            globals[key] = Pop();
        }

        public void Call()
        {
            object o = Pop();
            returnStack.Push(ip);
            varStack.Push(locals);
            locals = new Dictionary<object, object>();

            if (o is FunctionCallDelegate)
            {
                ((FunctionCallDelegate)o).Invoke(this);
                ip = returnStack.Pop();
                locals = varStack.Pop();
            }
            else if (o is double)
            {
                ip = (int)(double)o;
                Run();
            }
            else if (o is string)
            {
                ip = jmpAddresses[(string)o];
                Run();
            }
            else
            {
                RuntimeError.Invoke("Undefined Function", ip);
                ip = returnStack.Pop();
                locals = varStack.Pop();
                ip++;
                Run();
            }
        }

        public void Run()
        {
            object a, b;
            bool isRunning = true;
            while (isRunning)
            {
                if (ip >= instructions.Count)
                {
                    isRunning = false;
                    ip = instructions.Count - 1;
                    break;
                }

                switch (instructions[ip].opCode)
                {
                    case InstructionCodes.Push:
                        Push(instructions[ip].data);
                        ip++;
                        break;
                    case InstructionCodes.Pop:
                        Pop();
                        ip++;
                        break;
                    case InstructionCodes.Read:
                        a = Pop();
                        ip++;   //So ReadFunction Callback returns to the right place
                        if (ReadFunction != null)
                            ReadFunction.Invoke(a);
                        break;
                    case InstructionCodes.Write:
                        a = Pop();
                        if (WriteFunction != null)
                        {
                            if (a != null)
                                WriteFunction.Invoke(a.ToString());
                            else
                                WriteFunction.Invoke("Null");
                        }
                        ip++;
                        break;
                    case InstructionCodes.Load:
                        Load();
                        ip++;
                        break;
                    case InstructionCodes.Store:
                        Store();
                        ip++;
                        break;
                    case InstructionCodes.GLoad:
                        GLoad();
                        ip++;
                        break;
                    case InstructionCodes.GStore:
                        GStore();
                        ip++;
                        break;
                    case InstructionCodes.DLoad:
                        b = Pop();
                        a = Pop();
                        if (b == null || !(b is Dictionary<object, object>))
                            RuntimeError.Invoke("Attempting index non-dictionary value", ip);
                        else
                        {
                            if (((Dictionary<object, object>)b).ContainsKey(a))
                                Push(((Dictionary<object, object>)b)[a]);
                            else
                                Push(null);
                        }
                        ip++;
                        break;
                    case InstructionCodes.DStore:
                        b = Pop();
                        a = Pop();
                        if (b == null || !(b is Dictionary<object, object>))
                            RuntimeError.Invoke("Attempting index non-dictionary value", ip);
                        else
                        {
                            ((Dictionary<object, object>)b)[a] = Pop();
                        }
                        ip++;
                        break;
                    case InstructionCodes.Add:
                        b = Pop();
                        a = Pop();
                        if (a is string && b is double)
                            Push((string)a + b.ToString());
                        else if (a is double && b is string)
                            Push(a.ToString() + (string)b);
                        else if (a is string && b is string)
                            Push((string)a + (string)b);
                        else if (a is double && b is double)
                            Push((double)a + (double)b);
                        else
                            Push(null);
                        ip++;
                        break;
                    case InstructionCodes.Sub:
                        b = Pop();
                        a = Pop();
                        if (a is double && b is double)
                            Push((double)a - (double)b);
                        else
                            Push(null);
                        ip++;
                        break;
                    case InstructionCodes.Mul:
                        b = Pop();
                        a = Pop();
                        if (a is double && b is double)
                            Push((double)a * (double)b);
                        else
                            Push(null);
                        ip++;
                        break;
                    case InstructionCodes.Div:
                        b = Pop();
                        a = Pop();
                        if (a is double && b is double)
                            Push((double)a / (double)b);
                        else
                            Push(null);
                        ip++;
                        break;
                    case InstructionCodes.Neg:
                        a = Pop();
                        if (a is double)
                            Push(-(double)a);
                        else
                            Push(null);
                        ip++;
                        break;
                    case InstructionCodes.Not:
                        a = Pop();
                        if (a == null || a.GetType() != typeof(bool))
                            RuntimeError.Invoke("Attempting to use the 'NOT' operator on non-boolean", ip);
                        else
                        {
                            if ((bool)a == true)
                                Push(false);
                            else
                                Push(true);
                        }
                        ip++;
                        break;
                    case InstructionCodes.Jmp:
                        a = Pop();
                        if (a is double)
                            ip = (int)(double)a;
                        else if (a is string)
                            ip = jmpAddresses[(string)a];
                        else
                        {
                            RuntimeError.Invoke("Jump address must be number or label", ip);
                            ip++;
                        }
                        break;
                    case InstructionCodes.Jt:
                        a = Pop();
                        if (Convert.ToBoolean(Pop()) == true)
                        {
                            if (a is double)
                                ip = (int)(double)a;
                            else if (a is string)
                                ip = jmpAddresses[(string)a];
                            else
                            {
                                RuntimeError.Invoke("Jump address must be number or label", ip);
                                ip++;
                            }
                        }
                        else
                            ip++;
                        break;
                    case InstructionCodes.Eq:
                        b = Pop();
                        a = Pop();
                        if (a == null || b == null || a.GetType() != b.GetType())
                            RuntimeError.Invoke("Attempting to compare different types", ip);
                        else
                        {
                            if (a.Equals(b))
                                Push(true);
                            else
                                Push(false);
                        }
                        ip++;
                        break;
                    case InstructionCodes.Lt:
                        b = Pop();
                        a = Pop();
                        if (a == null || b == null || a.GetType() != typeof(double) || b.GetType() != typeof(double))
                            RuntimeError.Invoke("Attempting to compare non-numeric values", ip);
                        else
                        {
                            if ((double)a < (double)b)
                                Push(true);
                            else
                                Push(false);
                        }
                        ip++;
                        break;
                    case InstructionCodes.Le:
                        b = Pop();
                        a = Pop();
                        if (a == null || b == null || a.GetType() != typeof(double) || b.GetType() != typeof(double))
                            RuntimeError.Invoke("Attempting to compare non-numeric values", ip);
                        else
                        {
                            if ((double)a <= (double)b)
                                Push(true);
                            else
                                Push(false);
                        }
                        ip++;
                        break;
                    case InstructionCodes.Call:
                        a = Pop();
                        returnStack.Push(ip + 1);
                        varStack.Push(locals);
                        locals = new Dictionary<object, object>();

                        if (a is FunctionCallDelegate)
                        {
                            ((FunctionCallDelegate)a).Invoke(this);
                            ip = returnStack.Pop();
                            locals = varStack.Pop();
                        }
                        else if (a is double)
                            ip = (int)(double)a;
                        else if (a is string)
                            ip = jmpAddresses[(string)a];
                        else
                        {
                            RuntimeError.Invoke("Undefined Function", ip);
                            ip = returnStack.Pop();
                            locals = varStack.Pop();
                            ip++;
                        }
                        break;
                    case InstructionCodes.Ret:
                        ip = returnStack.Pop();
                        locals = varStack.Pop();
                        break;
                    case InstructionCodes.End:
                        isRunning = false;
                        break;
                }
            }
            ip++;
        }
    }
}
